Node多进程
Node 多进程开发入门
Node多进程核心是创建一个子进程,子进程依附在当前Node进程下面
核心类是 child_process
文档路径:http://nodejs.cn/api/child_process.html
子进程概念:是系统进行资源分配和调度的基本单位,是操作系统结构的基础
进程的概念只要有两点
- 进程是一个实体。每一个进程都有它的地址空间
- 进程是一个“执行中的程序”,存在嵌套关系
进程开线程,进程和线程的区别?
查看进程(os)
ps -ef
查看所有进程
Last login: Sun Feb 7 15:42:44 on ttys002
jolly@JollydeMacBook-Pro jolly-cli-dev % ps -ef
UID PID PPID C STIME TTY TIME CMD
0 1 0 0 301220 ?? 14:35.92 /sbin/launchd
0 112 1 0 301220 ?? 0:56.66 /usr/sbin/syslogd
0 113 1 0 301220 ?? 2:26.60 /usr/libexec/UserEventAgent (System)
0 116 1 0 301220 ?? 0:13.29 /System/Library/PrivateFrameworks/Uninstall.framework/Resources/uninstalld
0 117 1 0 301220 ?? 0:54.79 /usr/libexec/kextd
0 118 1 0 301220 ?? 5:35.10 /System/Library/Frameworks/CoreServices.framework/Versions/A/Frameworks/FSEvents.framework/Versions/A/Support/fseventsd
- UID 获得权限的用户的UID
- PID 进程的ID
ps -ef|grep PID
可帅选出对应进程
- PPID 父进程的ID,体现进程的嵌套关系
vscode 启动 node
调试时的父子进程关系图。体现了进程嵌套
5-2 child_process异步方法使用教程(exec&execFile)
const cp = require('child_process');
child_process用法:
异步用法
- exec
- execFile
- fork
- spawn
同步用法
- execSync
- execFileSync
- spawnSync
异步
exec
执行shell
命令cp.exec('ls -la', function(err, stdout, stderr){
console.log('err: ', err); // 错误
console.log('stdout: ', stdout); // 正常运行输出的结果,后面会有个换行
console.log('stderr: ', stderr); // 异常输出的结果
});jolly@JollydeMacBook-Pro child_process % node index.js
err: null
stdout: total 8
drwxr-xr-x 3 jolly staff 96 2 7 16:58 .
drwxr-xr-x 14 jolly staff 448 2 7 16:58 ..
-rw-r--r-- 1 jolly staff 189 2 7 16:59 index.js
stderr:- 第二个参数是一个 option 对象
cwd
设置执行的目录- timeout 设置执行命令超时的时间,默认是0(不超时)
- 第二个参数是一个 option 对象
execFile
执行shell
文件- 有四个参数
如果第一个参数不是路径,而是命令,那么通过
which 命令
找到对应的文件cp.execFile('ls', ['-la'], function(err, stdout, stderr){
console.log('err: ', err);
console.log('stdout: ', stdout);
console.log('stderr: ', stderr);
});和上面
exec
执行效果一样execFile
第二个参数是,传入文件的参数和
exec
的区别exec
支持更复杂的shell
命令:ls -la|grep node_modules
但
execFile
执行[-la|grep node_modules]
会报错,不能执行成功。因为grep
不是execFile
执行文件ls
的参数可以执行
.shell
文件:将ls -al|grep node_modules
写在.shell
文件中chmod +x test.shell
添加执行权限
test.shell
文件当中的内容ls -la|grep node_modules
echo $1$1
代表execFile
传入的第一个参数
exec
也能执行指定文件,但是不支持传入参数
spwan
执行shell
文件如果第一个参数不是路径,而是命令,那么通过
which 命令
找到对应的文件const child = cp.spawn(path.resolve(__dirname, 'test.shell'), ['-la'], {
cwd: path.resolve('.')
})
child.stdout.on('data', function(chunk) {
console.log('stdout: ', chunk.toString()); //一次输出一个字符串
});
child.stderr.on('data', function(chunk) {
console.log('stderr: ', chunk.toString());
});
child.on('error', e => { // 监听错误
process.exit(1);
});
child.on('exit', e => {// 监听执行成功后的退出事件
});option参数
stdio
选项用于配置在父进程和子进程之间建立的管道。值:pipe
默认值,在子进程和父进程之间创建一个管道。ignore
静默执行,不会收到反馈inherit
将相应的 stdio 流传给父进程或从父进程传入。将输入、输出、错误,绑定到父进程的process.stdin
、process.stdout
和process.stderr
上。直接能看到打印,还带动画(进度)信息 。
fork
使用node
执行命令- 一个参数:模块路径
- 和
require()
的区别require
加载的js
模块是在主进程中执行的。fork
测试在子进程中的执行的,执行的js
文件process.pid
会发生变化。
const child = cp.fork(path.resolve(__dirname, 'child.js'));
- 适合执行耗时任务。
同步
执行简单 shell
命令
execSync
执行shell
命令const stdout = cp.execSync('ls -la|grep node_modules');
console.log(stdout.toString()); // stdout 是个 bufferexecFileSync
执行shell
文件const stdout = cp.execFileSync('ls', ['-la']);
console.log(stdout.toString()); // stdout 是个 bufferspawnSync
执行shell
文件const ret = cp.spawnSync('ls', ['-la']);
console.log(ret.stdout.toString()); // ret 是个 bufferexec
、execFile
、fork
底层都是调用spawn
的何时使用
spawn
、exec
、execFile
spawn
适合耗时任务(比如:npm install
),需要不断日志。spawn
逐条执行命令exec
/execFile
:开销比较小的任务。整个执行完后返回
使用
path.resovle('./','shell.js')
时会报错,因为node的执行上下文,不一定是当前目录。要使用__dirname
,比如path.resovle(__dirname,'shell.js')
5-4 child_process fork用法及父子进程通信机制讲解
- fork主要是使用node来执行我们的命令。
- fork会执行两个进程 主进程与子进程。
- fork的本质也是调用spawn。
const child = cp.fork(path.resolve(__dirname, 'child.js'));
// 向子进程发送消息
child.send('hello child process', () => {
// child.disconnect(); // 断开主、子进程直接的连接,否则,命令行将进入等待状态
});
// 接受子进程消息
child.on('mssage', msg => {
console.log('msg: ', msg);
child.disconnect();
});
console.log('main pid', process.pid);
child.js
console.log('child process');
console.log('child pid', process.pid);
// 接受主进程消息
process.on('message', msg => {
console.log('msg: ', msg);
});
// 向主进程发送消息
process.send('hello main process');
执行结果
jolly@JollydeMacBook-Pro child_process % node index.js
main pid 61868
child process
child pid 61869
msg: hello child process
msg: hello main process
注意:子进程向主进程发送消息,容易造成死循环
windows 子进程执行 node
命令
cp.spawn('cmd', ['/c', 'node', '-e', code]); // '/c' 表示静默执行
child_process异步源码解析
源码解读解决的问题
exec
和execFile
到底有什么区别- 为什么
exec
/execFile
/fork
都是通过spawn
实现的,spawn
的作用到底是什么? - 为什么
spawn
调用后没有回调,而exec
/execFile
能够回调? - 为什么
spawn
调用后需要手动调用child.stdout.on('data', callback)
,这里的child.stdio
/child.stderr
到底是什么? - 为什么有
data
/error
/exit
/close
这么多种回调,它们的执行顺序到底是什么怎样的?
exec源码解读
exec
和execFile
的区别就是参数的区别在
execFile
中调用spawn
并且监听了stderr
和stdout
的data
事件,执行事件处理函数,exec
和execFile
的回调函数就是这个事件处理函数。所以exec
和execFile
有回调函数执行
exec
时,最后调用spawn
规范后的参数- 出现了
/bin/sh
envPairs
是环境变量
- 出现了
this._handle
是实际的进程执行
child.spawn
实际执行的是this._handle.spawn
,执行后开启新进程exec
执行后也是可以得到子进程对象的课程中调试源码时,
ls -la|grep node_modules
报错,而直接在bash
中运行不报错,是因为node
有执行的环境,执行的环境不一样(执行时,所在路径不一样)- 统一执行环境后(没有node_modules文件夹),
bash
中不会报错,但是没有结果。但调试还报错,是因为bash
处理了错误。我们代码中输出了错误而已
- 统一执行环境后(没有node_modules文件夹),
shell
的使用
方法一:直接执行shell文件
/bin/sh test.shell
方法二:直接执行
shell
语句/bin/sh -c "ls -la"
所以,没有
-c
要指定文件路径shell
命令ls -la
===/bin/sh -c "ls -la"
exec
源码精读
对象的扩展运算符进行浅拷贝
// 等同于 {...Object(true)}
{...true} // {}
// 等同于 {...Object(undefined)}
{...undefined} // {}
// 等同于 {...Object(null)}
{...null} // {}
{...'hello'}
// {0: "h", 1: "e", 2: "l", 3: "l", 4: "o"}
{ ...['a', 'b', 'c'] };
// {0: "a", 1: "b", 2: "c"}浅拷贝和深拷贝
- 浅拷贝:创建一个新对象,这个对象有着原始对象属性值的一份精确拷贝。如果属性是基本类型,拷贝的就是基本类型的值,如果属性是引用类型,拷贝的就是内存地址 ,所以如果其中一个对象改变了这个地址,就会影响到另一个对象。
- 深拷贝:将一个对象从内存中完整的拷贝一份出来,包括属性指向的引用类型,从堆内存中开辟一个新的区域存放新对象,且修改新对象不会影响原对象
注意:第二个参数传任何非
function
类型,都会产生获得一个对象。function normalizeExecArgs(command, options, callback) {
if (typeof options === 'function') {
callback = options;
options = undefined;
}
// 浅拷贝
options = { ...options }; // 将任意非 function 都将转化为参数
options.shell = typeof options.shell === 'string' ? options.shell : true; // 得到shell属性。
return {
file: command,
options: options, // options 至少有一个属性:shell
callback: callback
};
}
option.shell
可以是一个字符串,用来执行命令的文件。默认值: Unix 上是'/bin/sh'
,Windows 上是process.env.ComSpec
execFile
中首先对参数逐个判断,判断逻辑有点意思function execFile(file /* , args, options, callback */) {
let args = [];
let callback;
let options;
// 解析可选参数(第一个参数是 shell 文件路径),使用argument
let pos = 1;
if (pos < arguments.length && Array.isArray(arguments[pos])) { // 获得传入shell文件的参数
args = arguments[pos++];
} else if (pos < arguments.length && arguments[pos] == null) { // 第二个参数给 null 跳过第二个参数解析
pos++;
}
if (pos < arguments.length && typeof arguments[pos] === 'object') { // 参数是 Object 类型,认为是options
options = arguments[pos++];
} else if (pos < arguments.length && arguments[pos] == null) { // 参数值是 null,跳过
pos++;
}
if (pos < arguments.length && typeof arguments[pos] === 'function') { // 获得回调函数
callback = arguments[pos++];
}
if (!callback && pos < arguments.length && arguments[pos] != null) { // 经过以上步骤,传参了但没有解析到回调函数,报错。
throw new ERR_INVALID_ARG_VALUE('args', arguments[pos]);
}
...
}这样的参数解析,可以不用固定参数的顺序
查看
ERR_INVALID_ARG_VALUE
的报错const cp = require('child_process');
cp.exec('ls -la', null,'sdds');要传入第二个参数时,才能看见第三个参数的报错。原因见”对象的扩展运算符“
olly@192 child_process % node index.js
child_process.js:202
throw new ERR_INVALID_ARG_VALUE('args', arguments[pos]);
^
TypeError [ERR_INVALID_ARG_VALUE]: The argument 'args' is invalid. Received 'sdds'
at Object.execFile (child_process.js:202:11)
at Object.exec (child_process.js:145:25)
at Object.<anonymous> (/Users/jolly/Desktop/imooc/child_process/index.js:4:4)
at Module._compile (internal/modules/cjs/loader.js:959:30)
at Object.Module._extensions..js (internal/modules/cjs/loader.js:995:10)
at Module.load (internal/modules/cjs/loader.js:815:32)
at Function.Module._load (internal/modules/cjs/loader.js:727:14)
at Function.Module.runMain (internal/modules/cjs/loader.js:1047:10)
at internal/main/run_main_module.js:17:11 {
code: 'ERR_INVALID_ARG_VALUE'
}
数组的浅拷贝
//args = args.slice(0)
var a = [1, 2, 3];
var b = a.slice(0); // b: [1, 2, 3]
a === b; // falsespawn
中的命令拼接部分if (options.shell) {
const command = [file].concat(args).join(' '); // 拼接命令文件和传入的参数
// Set the shell, switches, and commands.
if (process.platform === 'win32') { // windows
if (typeof options.shell === 'string') // 自定义执行shell的文件
file = options.shell;
else
file = process.env.comspec || 'cmd.exe';
// '/d /s /c' is used only for cmd.exe.
if (/^(?:.*\\)?cmd(?:\.exe)?$/i.test(file)) { // 匹配任意路径下的 cmd.exe。这里指定了 cmd.exe 的路径
args = ['/d', '/s', '/c', `"${command}"`]; // '/d /s /c' 仅用于 cmd.exe.
options.windowsVerbatimArguments = true; // options 中的 windowsVerbatimArguments 参数
} else {
args = ['-c', command];
}
} else {
if (typeof options.shell === 'string')
file = options.shell;
else if (process.platform === 'android') // 安卓系统
file = '/system/bin/sh';
else
file = '/bin/sh'; // 默认使用 '/bin/sh'
args = ['-c', command];
}
}spawn
中的new ChildProcess()
EventEmitter.call(this);
之后,可以分发事件了。emit
分发on
监听
this._handle.onexit
进程执行完之后回调child.spawn
/ChildProcess.prototype.spawn
getValidStdio()
创建输入输出错误流输入流,子进程只有读权限
输出流,子进程只有写权限
new Pipe()
创建socket
通信,调用pipe_wrap
ipc
建立进程间的双向通信,在fork
时创建
循环建立父子进程 socket 通信
- socket 对象使用 on('data')监听
node_process回调调用流程
- Process 执行命令
child._handle.spawn(options)
执行命令exitCode
为0,表示执行成功,小于0表示失败
- 命令执行成功后,往”流“中写入信息,回调
onStreamRead
方法读取流中信息 onStreamRead
每读取完一条流中信息,调用一次onReadableStreamEnd
maybeClose()
中,判断所有socket
关闭后,关闭子进程- 两条线:
- 子进程的执行线
- 流的读取线
事件处理函数执行顺序
const child = cp.execFile('ls -la', function(err, stdout, stderr){
console.log('callback start-----------');
console.log('err: ', err);
console.log('stdout: ', stdout);
console.log('stderr: ', stderr);
console.log('callback end-----------');
});
child.on('error', chunk => {
console.log('error! ', chunk);
})
child.stdout.on('data', chunk => {
console.log('stdout data: ', chunk);
});
child.stderr.on('data', chunk => {
console.log('stderr data: ', chunk);
});
child.stdout.on('close', chunk => {
console.log('stdout close');
});
child.stderr.on('close', chunk => {
console.log('stderr close');
});
child.on('exit', (exitCode, signalCode) => {
console.log('exit! ', exitCode, ' ', signalCode);
});
child.on('close', (exitCode, signalCode) => {
console.log('close! ', exitCode, ' ', signalCode);
});
jolly@192 child_process % node index.js
stdout data: total 24
drwxr-xr-x 6 jolly staff 192 2 10 15:11 .
drwxr-xr-x 14 jolly staff 448 2 7 16:58 ..
drwxr-xr-x 3 jolly staff 96 2 10 15:11 .vscode
-rw-r--r-- 1 jolly staff 225 2 7 21:10 child.js
-rw-r--r-- 1 jolly staff 1901 2 11 16:50 index.js
-rwxr-xr-x 1 jolly staff 15 2 7 20:19 test.shell
exit! 0 null
stderr close
callback start-----------
err: null
stdout: total 24
drwxr-xr-x 6 jolly staff 192 2 10 15:11 .
drwxr-xr-x 14 jolly staff 448 2 7 16:58 ..
drwxr-xr-x 3 jolly staff 96 2 10 15:11 .vscode
-rw-r--r-- 1 jolly staff 225 2 7 21:10 child.js
-rw-r--r-- 1 jolly staff 1901 2 11 16:50 index.js
-rwxr-xr-x 1 jolly staff 15 2 7 20:19 test.shell
stderr:
callback end-----------
close! 0 null
stdout close
exec
执行和回调脑图
颜色说明:
- 黄色:回调执行过程
- 紫色:广播事件
- 绿色:进程
error
流程
关于 stderr
- 当命令执行失败,如
lss -ls
时ChildProcess.prototype.spawn()
中exitCode
是 0,并不小于0
Buffer
对象的字符串解码器
在 fork
流程,setupChannel(child, ipc)
设置,其中涉及 Buffer
对象的字符串解码。
string_decoder
模块提供了一个 API,用一种能保护已编码的多字节 UTF-8 和 UTF-16 字符的方式将 Buffer
对象解码为字符串。基本用法
const { StringDecoder } = require('string_decoder');
const decoder = new StringDecoder('utf8');
const cent = Buffer.from([0xC2, 0xA2]);
console.log(decoder.write(cent));
const euro = Buffer.from([0xE2, 0x82, 0xAC]);
console.log(decoder.write(euro));
fork
源码解读
- 剩余部分见
exec
执行脑图 - stdio
ipc
通信:[0, 1, 2, 'ipc']
process.execPath
拿到node
路径- 重点
getValidStdio(stdio, false)
- 执行
setupChannel(this, ipc)
,增强ipc
功能,在父、子进程之间启动ipc
:channel.readStart()
new Control(channel)
创建control
对象,用于执行ipc
的ref
和unref
方法- 有数据读取时,进入
channel.onread
- 执行
child.send()
调用target.send()
进行进程通信, 使用pipe
进行数据传递- 在执行的
js
文件中,process.send()
也是使用target.send()
进行通信
Node 多进程源码总结
- exec/execFile/spawn/fork的区别
exec
: 原理是调用bin/shell -c
执行我们传入的shell
脚本,调用execFile
,但传参做了处理execFile
:原理是直接执行我们传入的file
和args
,底层调用spawn
创建和执行子进程,但通过监听spawn
中广播的事件,建立了回调,且一次性将所有的stdout
和stderr
结果返回spawn
:原理是调用internal/child_process
,实例化了ChildProcess
子进程对象,再调用ChildProcess.prototype.spawn()
创建子进程并执行命令,底层调用了child._handle.spawn()
执行C++ process_wrap
中的spawn
方法。执行过程是异步的。执行完后,通过 pipe 进行单向数据通信,通信结束后,子进程发起child._handle.onexit
回调,同时 socket 会执行close
回调。fork
:原理是通过spawn
创建子进程和执行命令。使用node
执行命令,通过setupchannel
创建IPC
用于子进程和父进程之间的双向通信
- data/error/exit/close回调的区别
data
:主进程读取数据过程中,通过onStreamRead
发起回调error
:命令执行失败后发起的回调exit
:子进程关闭完成后发起的回调close
:子进程所有Socket
通信端口全部关闭后发起的回调stdout close
/stderr close
:特定的 PIPE 读取完成后调用onReadableStreamEnd()
关闭Socket
时发起的回调。
child_process同步源码解析
execSync
、 execFileSync
和 spawnSync
执行流程
execSync
、 execFileSync
和 spawnSync
的区别
execSync
和execFileSync
底层都是调用spawnSync
。但是execSync
和execFileSync
在调用spawnSync
之前的参数规范化逻辑不一样:execSync
和exec
都是调用normalizeExecArgs()
execFileSync
、spawn
及spawnSync
调用的是normalizeSpawnArguments()
spawnSync
返回的是child_process.spawnSync(opts)
中spawn_sync.spawn(options)
(C++
代码) 执行返回的结果result
execSync
和execFileSync
都将spawnSync
的执行结果做了同样的处理有错误,直接从主进程
threw
错误在
execFileSync('ls -la')
找不到文件时result.error
错误日志
child_process.js:642
throw err;
^
Error: spawnSync ls -la ENOENT
at Object.spawnSync (internal/child_process.js:1041:20)
at spawnSync (child_process.js:607:24)
at Object.execFileSync (child_process.js:634:15)
at Object.<anonymous> (/Users/jolly/Desktop/imooc/child_process/index.js:7:19)
at Module._compile (internal/modules/cjs/loader.js:959:30)
at Object.Module._extensions..js (internal/modules/cjs/loader.js:995:10)
at Module.load (internal/modules/cjs/loader.js:815:32)
at Function.Module._load (internal/modules/cjs/loader.js:727:14)
at Function.Module.runMain (internal/modules/cjs/loader.js:1047:10)
at internal/main/run_main_module.js:17:11 {
errno: 'ENOENT',
code: 'ENOENT',
syscall: 'spawnSync ls -la',
path: 'ls -la',
spawnargs: [],
error: [Circular],
status: null,
signal: null,
output: null,
pid: 6262,
stdout: null,
stderr: null
}result.stdout
和result.stderr
中是null
在命令本身错误时:
lss -la
result.error = undefined
result.stdout
中的ArrayBuffer
为空result.stderr
中ArrayBuffer
有值。result.stderr
中的信息,将通过process.stderr.write(ret.stderr)
打印在主进程的日志中:/bin/sh: lss: command not found
result.status !== 0
,打印result.stderr
之后,threw 错误信息
没有错误,返回执行成功的结果输出流
stdout
使用示例
const cp = require('child_process');
const path = require('path');
execSync
const stdout = cp.execSync('lss -la');
console.log('stdout: ', stdout.toString()); // 有异常不会执行execFileSync
const stdout = cp.execFileSync(path.resolve(__dirname, 'test.shell'));
console.log('stdout: ', stdout.toString()); // 有异常不会执行spawnSync
const result = cp.spawnSync(path.resolve(__dirname, 'test.shell'));
if (result.status === 0) { // 没有异常
console.log(result.stdout.toString())
} else {
console.log(result)
}
cluster
工作原理
单个 Node.js 实例运行在单个线程中。 为了充分利用多核系统,有时需要启用一组 Node.js 进程去处理负载任务。
cluster
模块可以创建共享服务器端口的子进程。
工作进程由 child_process.fork()
方法创建,因此它们可以使用 IPC 和父进程通信,从而使各进程交替处理连接服务。
require
加载内置模块解析
判断内置模块
源码
function NativeModule(id) {
this.filename = `${id}.js`;
this.id = id;
this.exports = {};
this.module = undefined;
this.exportKeys = undefined;
this.loaded = false;
this.loading = false;
this.canBeRequiredByUsers = !id.startsWith('internal/');
}
// ...
const {
moduleIds,
compileFunction
} = internalBinding('native_module');
NativeModule.map = new Map();
for (let i = 0; i < moduleIds.length; ++i) {
const id = moduleIds[i];
const mod = new NativeModule(id);
NativeModule.map.set(id, mod);
}
moduleIds
:内置模块的路径+文件ming。通过C++
代码拿到this.canBeRequiredByUsers
:是否是内置模块,moduleIds
中,internal/
文件夹下都是内置模块cluster
执行流程cluter
对象,是EventEmitter
对象的实例,具有event
emit
、on
等方法cluster.fork([env])
env
<object>
要添加到进程环境变量的键值对。- 返回:
cluster.worker
cluster.setupMaster([settings])
settings
<object>
- 用于修改默认的
fork
行为。 一旦调用,将会按照settings
对cluster.settings
进行设置。 - 所有的设置只对后来的
.fork()
调用有效,对之前的工作进程无影响。
process.execArgv
属性返回当·Node.js 进程被启动时,Node.js 特定的命令行选项。 这些选项在
process.argv
属性返回的数组中不会出现,并且这些选项中不会包括Node.js
的可执行脚本名称或者任何在脚本名称后面出现的选项。 这些选项在创建子进程时是有用的,因为他们包含了与父进程一样的执行环境信息。$ node --harmony script.js --version
process.execArgv
的结果:['--harmony']
process.argv
的结果:['/usr/local/bin/node', 'script.js', '--version']
setup
事件每当
cluster.setupMaster()
被调用时触发。work.process
是child_process.fork()
创建的进程在主进程中,访问
cluster.workers
获得工作进程信息。其是一个哈希表,储存了活跃的工作进程对象,使用id
作为键名。只能在主进程中访问internal/cluster/child.js
规定了工作进程的行为,这里定义了cluster.worker
,保存工作进程对象的引用。 在工程进程中可访问,指向当前工作进程。疑问
workerProcess
由child_process.fork()
创建'internalMessage'
事件怎么触发,返回的参数message
和handle
是什么?- 其
send
方法干了啥?
见
child_process
异步源码解析文章fork
部分,setupChannel(child, ipc)